#include <stdio.h>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <sstream>
#include <cmath>
#include <cstring>
#include "core.hpp"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

namespace freecraft
{

#if 0
bool Loader::objLoader(std::string &objName, BaseCube &obj)
{
    std::vector<unsigned int> vertexIndices, uvIndices, normalIndices;
    std::vector<glm::vec3> temp_vertices;
    std::vector<glm::vec2> temp_uvs;
    std::vector<glm::vec3> temp_normals;

    std::string path = "objects/" + objName + ".obj";
    printf("%s\n", path.c_str());

    FILE *file;
    fopen_s(&file, path.c_str(), "r");

    if (file == NULL)
    {
        printf("Impossible to open the file!\n");
        return false;
    }

    while (1)
    {
        char lineHeader[128];
        int res = fscanf_s(file, "%s", lineHeader, 128);
        if (res == EOF)
            break;

        if (strcmp(lineHeader, "v") == 0)
        {
            glm::vec3 vertex;
            fscanf_s(file, "%f %f %f\n", &vertex.x, &vertex.y, &vertex.z);
            temp_vertices.push_back(vertex);
        }
        else if (strcmp(lineHeader, "vt") == 0)
        {
            glm::vec2 uv;
            fscanf_s(file, "%f %f\n", &uv.x, &uv.y);
            temp_uvs.push_back(uv);
        }
        else if (strcmp(lineHeader, "vn") == 0)
        {
            glm::vec3 normal;
            fscanf_s(file, "%f %f %f\n", &normal.x, &normal.y, &normal.z);
            temp_normals.push_back(normal);
        }
        else if (strcmp(lineHeader, "f") == 0)
        {
            std::string vertex1, vertex2, vertex3;
            unsigned int vertexIndex[3], uvIndex[3], normalIndex[3];
            int matches = fscanf_s(file, "%d/%d/%d %d/%d/%d %d/%d/%d\n", 
                &vertexIndex[0], &uvIndex[0], &normalIndex[0],
                &vertexIndex[1], &uvIndex[1], &normalIndex[1],
                &vertexIndex[2], &uvIndex[2], &normalIndex[2]
            );

            if (matches != 9)
            {
                printf("File can't be read by our simple parser : (Try exporting with other options)\n");
                return false;
            }

            vertexIndices.push_back(vertexIndex[0]);
            vertexIndices.push_back(vertexIndex[1]);
            vertexIndices.push_back(vertexIndex[2]);
            uvIndices.push_back(uvIndex[0]);
            uvIndices.push_back(uvIndex[1]);
            uvIndices.push_back(uvIndex[2]);
            normalIndices.push_back(normalIndex[0]);
            normalIndices.push_back(normalIndex[1]);
            normalIndices.push_back(normalIndex[2]);
        }
        else
        {
            char stupidBuffer[1000];
            fgets(stupidBuffer, 1000, file);
        }
    }

    fclose(file);

    obj.vectices.clear();
    obj.uvs.clear();
    obj.normals.clear();

    for (unsigned int i=0; i<vertexIndices.size(); i++)
    {
        unsigned int vertexIndex = vertexIndices[i];
        unsigned int uvIndex = uvIndices[i];
        unsigned int normalIndex = normalIndices[i];

        glm::vec3 vertex = temp_vertices[vertexIndex - 1];
        glm::vec2 uv = temp_uvs[uvIndex - 1];
        glm::vec3 normal = temp_normals[normalIndex - 1];

        obj.vectices.push_back(vertex);
        obj.uvs.push_back(uv);
        obj.normals.push_back(normal);
    }

    return true;
}
#endif

AABB::AABB(): min(0), max(0)
{

}

AABB::AABB(const std::vector<glm::vec3> &vectices):
    vectices(vectices)
{
    this->updateMinMax();
}

AABB::AABB(std::vector<glm::vec3> &&vectices): 
    vectices(vectices)
{
    this->updateMinMax();
}

void AABB::updateMinMax()
{
    if (this->vectices.size() == 0)
        return;

    this->min = this->vectices[0];
    this->max = this->vectices[1];
    for (int i=0; i<this->vectices.size(); i++)
    {
        if (this->min.x > this->vectices[i].x)
            this->min.x = this->vectices[i].x;
        if (this->min.y > this->vectices[i].y)
            this->min.y = this->vectices[i].y;
        if (this->min.z > this->vectices[i].z)
            this->min.z = this->vectices[i].z;

        if (this->max.x < this->vectices[i].x)
            this->max.x = this->vectices[i].x;
        if (this->max.y < this->vectices[i].y)
            this->max.y = this->vectices[i].y;
        if (this->max.z < this->vectices[i].z)
            this->max.z = this->vectices[i].z;
    }
}

void AABB::updateMinMax(const std::vector<glm::vec3> &vectices)
{
    if (vectices.size() == 0)
        return;

    this->vectices = vectices;
    this->updateMinMax();
}

void AABB::updateMinMax(std::vector<glm::vec3> &&vectices)
{
    if (vectices.size() == 0)
        return;  

    this->vectices = vectices;
    this->updateMinMax();
}

Ray::Ray(glm::vec3 &pos, glm::vec3 &dir):
    pos(pos), dir(dir)
{

}

bool Ray::rayCheck(const freecraft::AABB &aabb, glm::vec3 &hit)
{
	float tmin = 0;
	float tmax = FLT_MAX;

	if (!this->tCalc(aabb.min.x, aabb.max.x, this->pos.x, this->dir.x, tmin, tmax)) return false;
	if (!this->tCalc(aabb.min.y, aabb.max.y, this->pos.y, this->dir.y, tmin, tmax)) return false;
	if (!this->tCalc(aabb.min.z, aabb.max.z, this->pos.z, this->dir.z, tmin, tmax)) return false;

	hit.x = this->pos.x + tmin * this->dir.x;
	hit.y = this->pos.y + tmin * this->dir.y;
	hit.z = this->pos.z + tmin * this->dir.z;

    return true;
}

bool Ray::rayCheck(glm::vec3 min, glm::vec3 max, glm::vec3& hit)
{
	AABB aabb;
	aabb.min = min;
	aabb.max = max;
	return this->rayCheck(aabb, hit);
}

bool Ray::rayCheck(Ray &ray, const freecraft::AABB &aabb, glm::vec3 &hit)
{
    return ray.rayCheck(aabb, hit); 
}

bool Ray::rayCheck(Ray& ray, glm::vec3 min, glm::vec3 max, glm::vec3& hit)
{
	AABB aabb;
	aabb.min = min;
	aabb.max = max;
	return ray.rayCheck(aabb, hit);
}

bool Ray::tCalc(float min, float max, float pos, float dir, float& tmin, float &tmax)
{
	if (fabs(dir) < 0.000001f)
	{
		if ((pos < min) || (pos > max))
			return false;
	}

	float t1 = (min - pos) / dir;
	float t2 = (max - pos) / dir;

	if (t1 > t2)
	{
		float temp = t1;
		t1 = t2;
		t2 = temp;
	}

	if (t1 > tmin) tmin = t1;
	if (t2 < tmax) tmax = t2;

	if (tmin > tmax)
		return false;

	return true;
}

Element::Element()
{
    this->elementId = ElementId::AIR;
}

Element::Element(ElementId id): elementId(id)
{
}

bool PacketVertex::operator<(const freecraft::PacketVertex &that) const
{
    return memcmp((void*)this, (void*)&that, sizeof(PacketVertex)) < 0;
}

bool PacketVertex::operator<(freecraft::PacketVertex &&that) const
{
    return memcmp((void*)this, (void*)&that, sizeof(PacketVertex)) < 0;
}

Object::Object()
{

}

bool Object::loadObject(std::string &path, freecraft::Object &obj)
{
    std::vector<unsigned short> vertexIndices, uvIndices, normalIndices;
    std::vector<glm::vec3> temp_vertices;
    std::vector<glm::vec2> temp_uvs;
    std::vector<glm::vec3> temp_normals;

    std::fstream file(path, std::fstream::in);

    if (file.bad())
    {
        std::cout << "Impossible to open the file!\n";
        return false;
    }

    while (1)
    {
        std::string lineHeader;
        file >> lineHeader;

        if (file.eof())
            break;

        if (lineHeader == "v")
        {
            glm::vec3 vertex;
            file >> vertex.x >> vertex.y >> vertex.z;
            temp_vertices.push_back(vertex);
        }
        else if (lineHeader == "vt")
        {
            glm::vec2 uv;
            file >> uv.x >> uv.y;
            temp_uvs.push_back(uv);
        }
        else if (lineHeader == "vn")
        {
            glm::vec3 normal;
            file >> normal.x >> normal.y >> normal.z;
            temp_normals.push_back(normal);
        }
        else if (lineHeader == "f")
        {
            unsigned int vertexIndex[3], uvIndex[3], normalIndex[3];

            std::string temp;
            std::getline(file, temp);
            std::size_t pos = 0;
            while ((pos = temp.find('/', pos + 1)) != std::string::npos)
            {
                temp.replace(pos, 1, " ");
            }
            std::stringstream ss(temp);
            ss >> vertexIndex[0] >> uvIndex[0] >> normalIndex[0] >>
                vertexIndex[1] >> uvIndex[1] >> normalIndex[1] >>
                vertexIndex[2] >> uvIndex[2] >> normalIndex[2];
            
            vertexIndices.push_back(vertexIndex[0]);
            vertexIndices.push_back(vertexIndex[1]);
            vertexIndices.push_back(vertexIndex[2]);
            uvIndices.push_back(uvIndex[0]);
            uvIndices.push_back(uvIndex[1]);
            uvIndices.push_back(uvIndex[2]);
            normalIndices.push_back(normalIndex[0]);
            normalIndices.push_back(normalIndex[1]);
            normalIndices.push_back(normalIndex[2]);
        }
        else
        {
            std::string stupid;
            std::getline(file, stupid);
        }
    }

    file.close();

    obj.vectices.clear();
    obj.uvs.clear();
    obj.normals.clear();
    obj.indices.clear();

    obj.aabb.updateMinMax(temp_vertices);

    std::map<PacketVertex, unsigned short> indicesMap;
    for (unsigned int i=0; i<vertexIndices.size(); i++)
    {
        unsigned short vertexIndex = vertexIndices[i];
        unsigned short uvIndex = uvIndices[i];
        unsigned short normalIndex = normalIndices[i];

        PacketVertex packed;
        packed.position = temp_vertices[vertexIndex - 1];
        packed.uv = temp_uvs[uvIndex - 1];
        packed.normal = temp_normals[normalIndex - 1];

        unsigned short index;

        if (findSimilarVertexIndex(packed, indicesMap, index))
        {
            obj.indices.push_back(index);
        }
        else
        {
            index = (unsigned short)indicesMap.size();
            indicesMap[packed] = index;
            obj.indices.push_back(index);
            obj.vectices.push_back(packed.position);
            obj.uvs.push_back(packed.uv);
            obj.normals.push_back(packed.normal);
        }
    }

    float *bufferData = obj.makeVertexBufferData();
    if (!bufferData) return false;

    GLuint vbo, ebo;
    glGenVertexArrays(1, &obj.vao);
    glGenBuffers(1, &vbo);
    glGenBuffers(1, &ebo);

    glBindVertexArray(obj.vao);

    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, obj.vectices.size() * 8 * sizeof(float), bufferData, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, obj.indices.size() * sizeof(unsigned short), &obj.indices[0], GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(3*sizeof(float)));
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(6*sizeof(float)));
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);

    delete bufferData;

    return true;
}

void Object::free()
{
    
}

bool Object::findSimilarVertexIndex(
    freecraft::PacketVertex &packed, 
    std::map<PacketVertex, unsigned short> &indices, 
    unsigned short &index)
{
    std::map<PacketVertex, unsigned short>::iterator it = indices.find(packed);
    if (it == indices.end())
    {
        return false;
    }

    index = it->second;
    return true;
}

float *Object::makeVertexBufferData()
{
    float *buffer = new float[this->vectices.size() * 8];

    for (int i=0; i<this->vectices.size(); i++)
    {
        int offset = i * 8;
        buffer[offset] = vectices[i].x;
        buffer[offset + 1] = vectices[i].y;
        buffer[offset + 2] = vectices[i].z;

        buffer[offset + 3] = normals[i].x;
        buffer[offset + 4] = normals[i].y;
        buffer[offset + 5] = normals[i].z;

        buffer[offset + 6] = uvs[i].x;
        buffer[offset + 7] = uvs[i].y;
    }

    return buffer;
}

bool Texture::loadTexture(const std::string& path, freecraft::Texture& texture)
{
    stbi_set_flip_vertically_on_load(true);
    unsigned char* data = stbi_load(path.c_str(), &texture.width, &texture.height, &texture.colorChannels, 4);

    if (!data)
    {
        std::cout << "load texture failed: " << path << std::endl;
        return false;
    }

    glGenTextures(1, &texture.id);
    glBindTexture(GL_TEXTURE_2D, texture.id);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
        texture.width, texture.height,
        0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);

    stbi_image_free(data);

    return true;
}

Resource * Resource::instance = NULL;
const std::string Resource::elementNames[] = 
{
    "air",
    "base",
    "stair"
};

const std::string Resource::objectNames[] =
{
    "base",
    "stair",
};

Object Resource::objects[] = {};

Resource::Resource()
{

}

void Resource::init()
{
    for (int i=0; i<(unsigned int)ObjectId::COUNT; i++)
    {
        std::string path = "objects/" + Resource::objectNames[i] + ".obj";

        // bool res = loadOBJ(path.c_str(), objects[i].vectices, objects[i].uvs, objects[i].normals);
        // objects[i].aabb.updateMinMax(objects[i].vectices);
        Object::loadObject(path, objects[i]);
    }
}

Resource* Resource::getInstance()
{
    if (instance == NULL)
    {
        instance = new Resource();

        instance->init();
    }

    return instance;
}

const Object &Resource::getObject(freecraft::ObjectId id)
{
    Resource *re = Resource::getInstance();
    return re->objects[(unsigned int) id];
}

Shader::Shader(const char* vspath, const char* fspath)
{
    // Create the shaders
    GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
    GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

    // Read the Vertex Shader code from the file
    std::string VertexShaderCode;
    std::ifstream VertexShaderStream(vspath, std::ios::in);
    if(VertexShaderStream.is_open()){
        std::stringstream sstr;
        sstr << VertexShaderStream.rdbuf();
        VertexShaderCode = sstr.str();
        VertexShaderStream.close();
    }else{
        printf("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ !\n", vspath);
        getchar();
        return;
    }

    // Read the Fragment Shader code from the file
    std::string FragmentShaderCode;
    std::ifstream FragmentShaderStream(fspath, std::ios::in);
    if(FragmentShaderStream.is_open()){
        std::stringstream sstr;
        sstr << FragmentShaderStream.rdbuf();
        FragmentShaderCode = sstr.str();
        FragmentShaderStream.close();
    }

    GLint Result = GL_FALSE;
    int InfoLogLength;


    // Compile Vertex Shader
    printf("Compiling shader : %s\n", vspath);
    char const * VertexSourcePointer = VertexShaderCode.c_str();
    glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
    glCompileShader(VertexShaderID);

    // Check Vertex Shader
    glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
    glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    if ( InfoLogLength > 0 ){
        std::vector<char> VertexShaderErrorMessage(InfoLogLength+1);
        glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
        printf("%s\n", &VertexShaderErrorMessage[0]);
    }

    // Compile Fragment Shader
    printf("Compiling shader : %s\n", fspath);
    char const * FragmentSourcePointer = FragmentShaderCode.c_str();
    glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
    glCompileShader(FragmentShaderID);

    // Check Fragment Shader
    glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
    glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    if ( InfoLogLength > 0 ){
        std::vector<char> FragmentShaderErrorMessage(InfoLogLength+1);
        glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
        printf("%s\n", &FragmentShaderErrorMessage[0]);
    }



    // Link the program
    printf("Linking program\n");
    id = glCreateProgram();
    glAttachShader(id, VertexShaderID);
    glAttachShader(id, FragmentShaderID);
    glLinkProgram(id);

    // Check the program
    glGetProgramiv(id, GL_LINK_STATUS, &Result);
    glGetProgramiv(id, GL_INFO_LOG_LENGTH, &InfoLogLength);
    if ( InfoLogLength > 0 ){
        std::vector<char> ProgramErrorMessage(InfoLogLength+1);
        glGetProgramInfoLog(id, InfoLogLength, NULL, &ProgramErrorMessage[0]);
        printf("%s\n", &ProgramErrorMessage[0]);
    }

    
    glDetachShader(id, VertexShaderID);
    glDetachShader(id, FragmentShaderID);
    
    glDeleteShader(VertexShaderID);
    glDeleteShader(FragmentShaderID);
}

void Shader::setMatrix(const std::string &name, const glm::mat4 &m)
{
    glUniformMatrix4fv(glGetUniformLocation(id, name.c_str()), 1, GL_FALSE, &m[0][0]);
}

void Shader::setInt(const std::string &name, int value)
{
    glUniform1i(glGetUniformLocation(id, name.c_str()), value);
}

void Shader::use()
{
    glUseProgram(id);
}

}
